﻿/* 
 * Copyright (c) Intel Corporation
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * -- Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * -- Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * -- Neither the name of the Intel Corporation nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE INTEL OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Web;
using DotNetOpenAuth;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth;
using DotNetOpenAuth.OAuth.ChannelElements;
using DotNetOpenAuth.OAuth.Messages;
using HttpServer;

namespace WorldServer
{
    /// <summary>
    /// Various states an OAuth token can be in
    /// </summary>
    public enum TokenAuthorizationState : int
    {
        /// <summary>
        /// An unauthorized request token
        /// </summary>
        UnauthorizedRequestToken = 0,
        /// <summary>
        /// An authorized request token
        /// </summary>
        AuthorizedRequestToken = 1,
        /// <summary>
        /// An authorized access token
        /// </summary>
        AccessToken = 2
    }

    public class OAuthToken
    {
        public string Secret;
        public TokenAuthorizationState State;

        public OAuthToken(string secret, TokenAuthorizationState state)
        {
            Secret = secret;
            State = state;
        }
    }

    public static class OpenAuthHelper
    {
        public static ServiceProviderDescription CreateServiceProviderDescription(Service service)
        {
            ServiceProviderDescription desc = new ServiceProviderDescription();
            desc.RequestTokenEndpoint = new MessageReceivingEndpoint(service.OAuthRequestToken, HttpDeliveryMethods.PostRequest);
            desc.UserAuthorizationEndpoint = new MessageReceivingEndpoint(service.OAuthAuthorizeToken, HttpDeliveryMethods.GetRequest);
            desc.AccessTokenEndpoint = new MessageReceivingEndpoint(service.OAuthGetAccessToken, HttpDeliveryMethods.PostRequest);
            desc.TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() };

            return desc;
        }

        public static void OpenAuthResponseToHttp(IHttpResponse httpResponse, OutgoingWebResponse openAuthResponse)
        {
            httpResponse.Status = openAuthResponse.Status;
            foreach (string key in openAuthResponse.Headers.Keys)
                httpResponse.AddHeader(key, openAuthResponse.Headers[key]);
            if (!String.IsNullOrEmpty(openAuthResponse.Body))
                httpResponse.AddToBody(openAuthResponse.Body);
        }

        public static HttpRequestInfo GetRequestInfo(IHttpRequest request)
        {
            // Combine HTTP headers and URL query values
            WebHeaderCollection headers = new WebHeaderCollection();
            try { headers.Add(request.Headers); }
            catch (Exception) { }

            string rawUrl = request.Uri.AbsolutePath + request.Uri.Query + request.Uri.Fragment;
            return new HttpRequestInfo(request.Method, request.Uri, rawUrl, headers, request.Body);
        }

        public static string CreateQueryString(IDictionary<string, string> args)
        {
            if (args.Count == 0)
            {
                return String.Empty;
            }
            System.Text.StringBuilder sb = new System.Text.StringBuilder(args.Count * 10);

            foreach (KeyValuePair<string, string> arg in args)
            {
                sb.Append(HttpUtility.UrlEncode(arg.Key));
                sb.Append('=');
                sb.Append(HttpUtility.UrlEncode(arg.Value));
                sb.Append('&');
            }
            sb.Length--; // remove trailing &

            return sb.ToString();
        }

        public static Dictionary<string, string> QueryStringToDictionary(string queryString)
        {
            NameValueCollection values = HttpUtility.ParseQueryString(queryString);
            Dictionary<string, string> dictionary = new Dictionary<string, string>(values.Count);

            for (int i = 0; i < values.Count; i++)
            {
                string key = values.GetKey(i);
                dictionary[key] = values[key];
            }

            return dictionary;
        }

        public static string GetQueryValue(string query, string key)
        {
            try
            {
                NameValueCollection queryValues = HttpUtility.ParseQueryString(query);
                string[] values = queryValues.GetValues(key);
                if (values != null && values.Length > 0)
                    return values[0];
            }
            catch (Exception) { }

            return null;
        }
    }

    public class InMemoryTokenManager : IConsumerTokenManager
    {
        #region Classes

        private class TokenInfo : IServiceProviderRequestToken
        {
            internal TokenInfo()
            {
                this.CreatedOn = DateTime.Now;
            }

            public string ConsumerKey { get; set; }
            public DateTime CreatedOn { get; set; }
            public string Token { get; set; }
            public string VerificationCode { get; set; }
            public Uri Callback { get; set; }
            public Version ConsumerVersion { get; set; }
            internal string Secret { get; set; }
        }

        private class ConsumerInfo : IConsumerDescription
        {
            public string Key { get; set; }
            public string Secret { get; set; }
            public X509Certificate2 Certificate { get; set; }
            public Uri Callback { get; set; }
            public VerificationCodeFormat VerificationCodeFormat { get; set; }
            public int VerificationCodeLength { get; set; }
        }

        #endregion Classes

        private string consumerKey;
        private string consumerSecret;
        private Dictionary<string, TokenInfo> tokens = new Dictionary<string, TokenInfo>();

        /// <summary>
        /// Request tokens that have been issued, and whether they have been authorized yet.
        /// </summary>
        private Dictionary<string, bool> requestTokens = new Dictionary<string, bool>();

        /// <summary>
        /// Access tokens that have been issued and have not yet expired.
        /// </summary>
        private Dictionary<string, string> accessTokens = new Dictionary<string, string>();

        public InMemoryTokenManager(string consumerKey, string consumerSecret)
        {
            this.consumerKey = consumerKey;
            this.consumerSecret = consumerSecret;
        }

        #region IConsumerTokenManager Members

        public string ConsumerKey
        {
            get { return consumerKey; }
        }

        public string ConsumerSecret
        {
            get { return consumerSecret; }
        }

        #endregion

        #region ITokenManager Members

        public string GetTokenSecret(string token)
        {
            string tokenFixed = token.Replace(' ', '+');
            return this.tokens[tokenFixed].Secret;
        }

        public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response)
        {
            string requestTokenFixed = response.Token.Replace(' ', '+');
            this.tokens.Add(requestTokenFixed, new TokenInfo { ConsumerKey = request.ConsumerKey, Token = response.Token, Secret = response.TokenSecret });
            this.requestTokens.Add(requestTokenFixed, false);
        }

        /// <summary>
        /// Checks whether a given request token has already been authorized
        /// by some user for use by the Consumer that requested it.
        /// </summary>
        /// <param name="requestToken">The Consumer's request token.</param>
        /// <returns>
        /// True if the request token has already been fully authorized by the user
        /// who owns the relevant protected resources.  False if the token has not yet
        /// been authorized, has expired or does not exist.
        /// </returns>
        public bool IsRequestTokenAuthorized(string requestToken)
        {
            string requestTokenFixed = requestToken.Replace(' ', '+');
            return this.requestTokens[requestTokenFixed];
        }

        public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret)
        {
            string requestTokenFixed = requestToken.Replace(' ', '+');
            string accessTokenFixed = accessToken.Replace(' ', '+');

            this.requestTokens.Remove(requestTokenFixed);
            this.accessTokens.Add(accessTokenFixed, accessToken);
            this.tokens.Remove(requestTokenFixed);
            this.tokens.Add(accessTokenFixed, new TokenInfo { Token = accessToken, Secret = accessTokenSecret });
        }

        /// <summary>
        /// Classifies a token as a request token or an access token.
        /// </summary>
        /// <param name="token">The token to classify.</param>
        /// <returns>Request or Access token, or invalid if the token is not recognized.</returns>
        public TokenType GetTokenType(string token)
        {
            string tokenFixed = token.Replace(' ', '+');

            if (this.requestTokens.ContainsKey(tokenFixed))
            {
                return TokenType.RequestToken;
            }
            else if (this.accessTokens.ContainsKey(tokenFixed))
            {
                return TokenType.AccessToken;
            }
            else
            {
                return TokenType.InvalidToken;
            }
        }

        #endregion
    }
}
